package com.zxc.ompz;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import javax.microedition.io.Connector;
import javax.microedition.io.ContentConnection;
import javax.microedition.io.file.FileConnection;
import javax.microedition.media.Control;
import javax.microedition.media.Manager;
import javax.microedition.media.MediaException;
import javax.microedition.media.Player;
import javax.microedition.media.PlayerListener;
import javax.microedition.media.protocol.ContentDescriptor;
import javax.microedition.media.protocol.DataSource;
import javax.microedition.media.protocol.SourceStream;
/**
 * This is a online media stream player implementation, which refers mainly to 
 * blackberry sample "StreamingPlayer" but use a new thread-safe circular buffer 
 * implementation named "MediaBuffer". 
 *
 * @author ZXC (zhang.jianchun1@gmail.com)
 *
 */
public class MediaStreamPlayer implements PlayerListener{

	/****** STREAMING PLAYER STATE CONSTANTS ******/
	/** Log level **/
	private int logLevel = 0;
	/** Log event level defintion **/
	private static final int INFORMATIONAL = 0;
	private static final int DEBUGING      = 3;
	private static final int ERROR         = 6;
	private static final int EXCEPTION     = 7;
	
	/** Media stream player state definitions **/
	private int state = 4;
	/** Initial state when initialized using one of its constructors. **/ 
	public static final int UNREALIZED = 0;
	/** MediaStreamPlayer state when realize() is called */
	public static final int REALIZED = 1;
	/** MediaStreamPlayer state when prefetch() is called */
	public static final int PREFETCHED = 2;
	/** MediaStreamPlayer state when start() is called */
	public static final int STARTED = 3;	
	/** MediaStreamPlayer state when close() is called */
	public static final int CLOSED = 4;	
	
	/** Media bit rate **/
	private int bitRate = 128000;
	/** Underlying data source **/
	private MediaStreamSource mediaStreamSource;
	/** Connection type definition **/
	private int sourceType = 1;
	/** Media stream source is local **/
	private final int CONNECTION_LOCAL = 0;
	/** Media stream source is remote **/
	private final int CONNECTION_REMOTE= 1;
	
	/** Content connection **/
	private ContentConnection contentConn;
	/** File connection **/
	private FileConnection fileConn;
	/** Media stream url **/
	private String locator;
	/** Connection method **/
	//private String connString;
	/** Local file name **/
	private String fileName;
	/** Media content type **/
	private String contentType;
	/** Media content length **/
	private long contentLength;
	/** Input stream from local file or url **/
	private InputStream inStream;
	/** Output stream to local file **/
	private OutputStream outStream;
	
	/** Underlying circular media buffer **/
	private MediaBuffer buffer;
	/** For MediaBuffer, fixed buffer size **/
	private int bufferSize = (bitRate/8)*60;
	/** For MediaBuffer, minimum preserved stream size in buffer **/
	private int minBufferResv = bufferSize - (bitRate/8)*50;
	/** A flag to stop buffer observer **/
	private BufferObserver bufferObserver;
	
	/** Underlying media stream **/
	private MediaStream mediaStream;
	/** For MediaStream, read limit size at a time from MediaBuffer **/
	private int readLimit = (bitRate/8);
	/** A flag of user-initated seek **/
	private boolean userSeek = false;
	
	/** Underlying stream down loader **/
	private StreamLoader streamLoader;
	/** For StreamLoader, read trunk size at a time from connection **/
	private int READ_CHUNK = (bitRate/8);
	/** A flag to confirm media stream load complete **/
	private boolean mediaStreamEnd = false;

	/** Save to local disk **/
	private String dir = "file:///SDCard/Ompz/";
	
	/** Internal media player **/
	private Player player;
	/** External application listener **/
	private MediaStreamPlayerListener listener;
	
	/** Reference of player timer **/
	private PlayerTimer playerTimer;
	
	/** A lock to synchronize SEEK action **/
	private Object lockSeek = new Object();;
	/** A lock to synchronize SEEK, READ and WRITE **/
	private Object lockSRW = new Object();;

	/**
	 * Constructor
	 * 
	 * @param locator - media stream url
	 * @param connString - blackberry connection method
	 */
	public MediaStreamPlayer(String locator, String type) {
		log(DEBUGING, "MediaStreamPlayer(" + locator + "," + type + ")");
		this.locator = locator;
		this.contentType = type;
		this.state = UNREALIZED;
		if(locator.toLowerCase().startsWith("http://")){
			sourceType = CONNECTION_REMOTE;
			log(DEBUGING, "sourceType: remote(http)"); 
		}
		else if(locator.toLowerCase().startsWith("https://")){
			sourceType = CONNECTION_REMOTE;
			log(DEBUGING, "sourceType: remote(https)"); 
		}
		else if(locator.toLowerCase().startsWith("file://")){
			sourceType = CONNECTION_LOCAL;
			log(DEBUGING, "sourceType: local");
			if(type==null){
				throw new IllegalArgumentException("Type can not be null!");
			}
		}
		else{
			this.locator = null;
			throw new IllegalArgumentException("Locator is not valid.");
		}
	}
	/**
	 * Creates the underlying Player object and calls realize() on it. It also puts this StreamingPlayer in the 
	 * REALIZED state. Calling it when StreamingPlayer is not in UNREALIZED state has no effect. Also initializes
	 * the buffer of this StreamingPlayer.
	 * @throws IOException	Thrown by underlying Player while calling Manager.createPlayer().
	 * @throws MediaException	Thrown by underlying Player while calling Manager.createPlayer().
	 */
	public void realize() throws MediaException, IOException{	
		log(DEBUGING, "MediaStreamPlayer.realize()");
		buffer = new MediaBuffer(bufferSize);
		log(DEBUGING, "new MediaBuffer("+bufferSize+")");
		int s=getState();
		if(s==UNREALIZED){
			mediaStreamSource = new MediaStreamSource();
			player = Manager.createPlayer(mediaStreamSource);
			player.addPlayerListener(this);
			player.realize();
			log(DEBUGING, "REALIZE: player.realize()");
			state = REALIZED;
		}
	}
	/**
	 * Puts this StreamingPlayer in the PREFETCHED state. 
	 * If this StreamingPlayer is in REALIZED state, simply calls prefetch() on the underlying Player. 
	 * If this StreamingPlayer is in UNREALIZED state, calls StreamingPlayer.realize() and then Player.prefetch(). 
	 * Otherwise, this method does nothing.
	 * @throws MediaException	Thrown by Player.prefetch() or StreamingPlayer.realize().
	 * @throws IOException	Thrown by StreamingPlayer.realize().
	 */
	public void prefetch() throws MediaException, IOException{
		log(DEBUGING, "MediaStreamPlayer.prefetch()");
		int s = getState();
		if(s==UNREALIZED){
			realize();		
		}
		if(player!=null){
			player.prefetch();
			state = PREFETCHED;
		}
		bufferObserver = new BufferObserver();
		bufferObserver.start();
	}
	/**
	 * Release the scarce or exclusive resources like the audio device acquired by the Player.
	 * When deallocate returns, the Player is in the UNREALIZED or REALIZED state.
	 * If the Player is blocked at the realize call while realizing, calling deallocate
	 * unblocks the realize call and returns the Player to the UNREALIZED state. Otherwise, 
	 * calling deallocate returns the Player to the REALIZED state.
	 * 
	 * If deallocate is called when the Player is in the UNREALIZED or REALIZED state, the
	 * request is ignored.
	 * 
	 * If the Player is STARTED when deallocate is called, deallocate will implicitly call 
	 * stop on the Player.
	 */
	public void deallocate(){
		log(DEBUGING, "MediaStreamPlayer.deallocate()");
		int s = getState();
		switch(s){
		case UNREALIZED:
		case REALIZED:
			log(DEBUGING, "DEALLOCATE: unrealized or realized");
			break;
		case PREFETCHED:
		case STARTED:
			state=REALIZED;
			player.deallocate();
			break;
		}
	}
	/**
	 * Starts the playback by calling Player.start() and puts this StreamingPlayer in STARTED state. 
	 * If StreamingPlayer is in UNREALIZED state, calls StreamingPlayer.realize(), StreamingPlayer.prefetch() and Player.start().
	 * If StreamingPlayer is in REALIZED state, calls StreamingPlayer.prefetch() and Player.start().
	 * If StreamingPlayer is in PREFETCHED state, calls Player.start().
	 * Otherwise, the call is ignored.
	 * 
	 * @throws MediaException Thrown by StreamingPlayer.realize(), StreamingPlayer.prefetch() or Player.start()
	 * @throws IOException Thrown by StreamingPlayer.realize()
	 */
	public void start() throws MediaException, IOException{
		log(DEBUGING, "MediaStreamPlayer.start()");
		int s = getState();
		if(s==UNREALIZED || s==REALIZED){
			prefetch();
		}
		if (player!=null){
			player.start();
			state = STARTED;
			playerTimer = new PlayerTimer();
			playerTimer.start();
		}
	}
	/**
	 * Pauses the playback by calling Player.stop() and puts this StreamingPlayer in PREFETCHED state.
	 * If StreamingPlayer is in STARTED state, calls Player.stop()
	 * Otherwise, the call is ignored.  
	 * 
	 * @throws MediaException	Thrown by Player.stop().
	 */
	public void stop() throws MediaException{
		log(DEBUGING, "MediaStreamPlayer.stop()");
		if(player!=null && player.getState()==Player.STARTED){
			player.stop();
			state = PREFETCHED;
			if (playerTimer!=null){
				playerTimer.stop();
				playerTimer=null;
			}
		}		
	}
	/**
	 * Close media stream player
	 * @throws MediaException 
	 */
	public void close() throws MediaException {
		log(DEBUGING, "MediaStreamPlayer.close()");
		if (player != null){
			player.close();
			player=null;
		}
		if (mediaStreamSource != null){
			mediaStreamSource.disconnect();
			mediaStreamSource = null;
		}
		if (streamLoader != null){
			streamLoader.stop();
			streamLoader=null;
		}
		if (playerTimer!=null){
			playerTimer.stop();
			playerTimer=null;
		}
		if (bufferObserver!=null){
			bufferObserver.stop();
			bufferObserver=null;
		}
		buffer = null;
		state = CLOSED;
	}
	/**
	 * Returns the content type of the media stream. 
	 * @return
	 */
	public String getContentType(){
		log(DEBUGING, "MediaStreamPlayer.getContentType()=" + contentType);
		return contentType;
	}
	/**
	 * Gets the duration of the media. The value returned is the media's duration
	 * when played at the default rate. If the duration cannot be determined (for 
	 * example, the Player is presenting live media) getDuration returns TIME_UNKNOWN.
	 * 
	 * @return	The duration in microseconds or TIME_UNKNOWN.
	 * @throws	IllegalStateException - Thrown if the Player is in the CLOSED state. 
	 * Or if the StreamingPlayer is in UNREALIZED state.
	 */
	public long getDuration() throws IllegalStateException{
		log(DEBUGING, "MediaStreamPlayer.getDuration()");
		if(getState()==UNREALIZED){
			log(0, "FAILED! SP.getDuration()");
			throw new IllegalStateException("Cannot be called in UNREALIZED state");
		}	
		long duration = player.getDuration();
		log(DEBUGING, "getDuration()=" + duration);
		return duration;
	}
	/**
	 * Gets the content length of the media source.
	 * @return	the content length of the media source.
	 * @throws IllegalStateException	If StreamingPlayer is in UNREALIZED state.
	 */
	public long getContentLength() throws IllegalStateException{
		log(DEBUGING, "MediaStreamPlayer.getContentLength()");
		if(getState()==UNREALIZED){
			log(EXCEPTION, "FAILED! MediaStreamPlayer.getContentLength()");
			throw new IllegalStateException("Cannot be called in UNREALIZED state");
		}	
		log(DEBUGING, "getContentLength()=" + contentLength);
		return contentLength;
	}
	/**
	 * Sets the Player's media time. 
	 * For some media types, setting the media time may not be very accurate. The returned 
	 * value will indicate the actual media time set.
	 * 
	 * Setting the media time to negative values will effectively set the media time to zero. 
	 * Setting the media time to beyond the duration of the media will set the time to the 
	 * end of media.
	 * 
	 * There are some media types that cannot support the setting of media time. Calling 
	 * setMediaTime will throw a MediaException in those cases.
	 * 
	 * @param	microseconds 	The new media time in microseconds.
	 * @return	The actual media time set in microseconds.
	 * @throws	IllegalStateException - Thrown if the Player is in the UNREALIZED or CLOSED state.
	 * @throws	MediaException - Thrown if the media time cannot be set.	 * 
	 */
	public long setMediaTime(long microseconds) throws MediaException, IllegalStateException{	
		log(DEBUGING, "MediaStreamPlayer.setMediaTime(" + microseconds + ")");
		userSeek = true;
		if(getState()==UNREALIZED){
			log(EXCEPTION, "FAILED! MediaStreamPlayer.setMediaTime()");
			throw new IllegalStateException("Cannot be called in UNREALIZED state");
		}					
		long time = player.setMediaTime(microseconds);
		log(DEBUGING, "MediaStreamPlayer.setMediaTime("+microseconds+")=" + time);
		return time;
	}
	
	/**
	 * Gets this Player's current media time. If the media time cannot be determined, getMediaTime returns TIME_UNKNOWN.
	 * 
	 * @return	The current media time in microseconds or TIME_UNKNOWN.
	 * @throws	IllegalStateException	Thrown if the Player is in the CLOSED state. Or if the StreamingPlayer is in UNREALIZED state.
	 */
	public long getMediaTime() throws IllegalStateException{
		log(DEBUGING, "MediaStreamPlayer.getMediaTime()");
		if(getState()==UNREALIZED){
			log(EXCEPTION, "FAILED! MediaStreamPlayer.getMediaTime()");
			throw new IllegalStateException("Cannot be called in UNREALIZED state");
		}	
		long time = player.getMediaTime();
		log(DEBUGING, "getMediaTime()=" + time);
		return time;
	}
	/**
	 * Obtain the object that implements the specified Control interface.
	 * 
	 * If the specified Control interface is not supported then null is returned.
	 * 
	 * If the Controllable supports multiple objects that implement the same specified Control interface, only one of them will be returned. To obtain all the Control's of that type, use the getControls method and check the list for the requested type.
	 * @param	controlType	the class name of the Control. The class name should be given either as the fully-qualified name of the class; or if the package of the class is not given, the package javax.microedition.media.control is assumed.
	 * @return	the object that implements the control, or null.
	 * @throws	IllegalArgumentException	Thrown if controlType is null.
	 * @throws	IllegalStateException	Thrown if getControl is called in a wrong state. See Player for more details.
	 */
	public Control getControl(String controlType){
		log(DEBUGING, "MediaStreamPlayer.getControl("+controlType+")");
		return player.getControl(controlType);
	}
	/**
	 * Obtain the collection of Controls from the object that implements this interface.
	 * 
	 * The list of Control objects returned will not contain any duplicates. And the list will not change over time.
	 * 
	 * If no Control is supported, a zero length array is returned.
	 * @return	the collection of Control objects.
	 */
	public Control[] getControls(){
		log(DEBUGING, "MediaStreamPlayer.getControls()");
		return player.getControls();
	}
	
	/**
	 * Gets the current state of this StreamingPlayer.  
	 * @return	One of  UNREALIZED, REALIZED, PREFETCHED, STARTED and CLOSED. 
	 */
	public int getState(){
		log(DEBUGING, "MediaStreamPlayer.getState()=" + state);
		return state;
	}
	/**
	 * Registers a MediaStreamPlayerListener for this player. There can be only one 
	 * MediaStreamPlayerListener assigned to a StreamingPlayer at a time.
	 * 
	 * @param listener	A MediaStreamPlayerListener implementation.
	 */
	public void addMediaStreamPlayerListener(MediaStreamPlayerListener listener) {
		log(DEBUGING, "MediaStreamPlayerListener registered");
		this.listener = listener;
	}
	
	/**
	 * Removes a MediaStreamPlayerListener from this player.
	 * 
	 * @param listener	A MediaStreamPlayerListener implementation.
	 */
	public void removeMediaStreamPlayerListener(MediaStreamPlayerListener listener){
		log(DEBUGING, "MediaStreamPlayerListener removed");
		this.listener = null;
	}
	/**
	 * Set the player's log level
	 * 
	 * @param level - the lowest level
	 */
	public void setMediaStreamPlayerLogLevel(int level){
		log(DEBUGING, "MediaStreamPlayer.setMediaStreamPlayerLogLevel("+level+")");
		if (level>=0 && level<=7){
			this.logLevel = level;
		}
		else{
			throw new IllegalArgumentException("Level value should be 0-7");
		}
	}
	public String getMediaName(){
		log(DEBUGING, "MediaStreamPlayer.getMediaName()");
		return fileName;
	}
	/**
	 * log player events
	 * 
	 * @param level	the level of log event
	 * @param msg	the log message
	 */
	private void log(int level, String msg){
		if (listener!=null && level>=logLevel){
			listener.playerLog(level, msg);
		}
	}
	
	
	
	/************************* MediaStreamSource Begin *************************/
	public class MediaStreamSource extends DataSource {
		/**
		 * Constructs a new MediaStreamSource
		 * 
		 * @param locator	URL of the media stream source
		 * @param connString	BlackBerry connection method
		 */
		public MediaStreamSource() {
			super(locator);
			log(DEBUGING, "MediaStreamSource()");
			if (locator.indexOf(';')<0){
				fileName = locator.substring(locator.lastIndexOf('/')+1);
			}
			else{
				fileName = locator.substring(locator.lastIndexOf('/')+1, locator.indexOf(';'));
			}
			log(DEBUGING, "fileName="+fileName);
		}
		/**
		 * Open a connection to the media stream source 
		 */
		public void connect() throws IOException {	
			log(DEBUGING, "MediaStreamSource.connect()");
			if (sourceType==CONNECTION_LOCAL){
				fileConn = (FileConnection) Connector.open(dir + fileName, Connector.READ_WRITE);
				log(DEBUGING, "CONNECT: (FileConnection) Connector.open("+dir+fileName+",READ_WRITE");
				contentLength = fileConn.fileSize();
				log(DEBUGING, "CONNECT: fileSize()=" + contentLength);
				inStream = fileConn.openInputStream();
				log(DEBUGING, "CONNECT: inStream = fileConn.openInputStream()");
				outStream = null;
			}
			else if(sourceType==CONNECTION_REMOTE){
				fileConn = (FileConnection) Connector.open(dir + fileName, Connector.READ_WRITE);
				log(DEBUGING, "CONNECT: (FileConnection) Connector.open("+dir+fileName+",READ_WRITE");
				contentConn = (ContentConnection) Connector.open(getLocator(), Connector.READ_WRITE);
				log(DEBUGING, "CONNECT: (ContentConnection) Connector.open");
				contentType = contentConn.getType();
				log(DEBUGING, "CONNECT: getType()=" + contentType);
				contentLength = contentConn.getLength();
				log(DEBUGING, "CONNECT: getLength()=" + contentLength);
				inStream = contentConn.openInputStream();
				log(DEBUGING, "CONNECT: inStream = contentConn.openInputStream()");
				if (!fileConn.exists()){
					fileConn.create();
					log(DEBUGING, "CONNECT: fileConn.create()");
				}
				outStream = fileConn.openOutputStream();
				log(DEBUGING, "CONNECT: outStream = fileConn.openOutputStream()");
			}
			mediaStream = new MediaStream(contentType);
			log(DEBUGING, "CONNECT: new MediaStream(" + contentType + ")");
			notifyPlayerEvent(MediaStreamPlayerListener.CONNECTION_OPEN, null);
		}
		/**
		 * Close the connection to the source described by the locator and free 
		 * resources used to maintain the connection.
		 */
		public void disconnect() {
			log(DEBUGING, "MediaStreamSource.disconnect()");
			if (streamLoader != null && streamLoader.isAlive()) {
				stop();
			}
			if (mediaStream != null) {
				mediaStream = null;
			}
			if (inStream != null) {
				try {
					inStream.close();
				}
				catch (IOException e) {
					log(EXCEPTION, "DISCONNECT: inStream.close():" + e.toString());
				}
				inStream = null;
				log(DEBUGING, "DISCONNECT: inStream.close()");
			}
			if (outStream != null) {
				try {
					outStream.close();
				}
				catch (IOException e) {
					log(EXCEPTION, "DISCONNECT: outStream.close():" + e.toString());
				}
				outStream = null;
				log(DEBUGING, "DISCONNECT: outStream.close()");
			}
			if (fileConn != null) {
				try {
					fileConn.close();
				}
				catch (IOException e) {
					log(EXCEPTION, "DISCONNECT: fileConn.close():" + e.toString());
				}
				fileConn = null;
				log(DEBUGING, "DISCONNECT: fileConn.close()");
			}
			if (contentConn != null) {
				try {
					contentConn.close();
				}
				catch (IOException e) {
					log(EXCEPTION, "DISCONNECT: contentConn.close():" + e.toString());
				}
				contentConn = null;
				log(DEBUGING, "DISCONNECT: contentConn.close()");
			}
			notifyPlayerEvent(MediaStreamPlayerListener.CONNECTION_CLOSE, null);
		}
		/**
		 * Get a sting that describes the content-type of the media that 
		 * the source is providing.
		 */
		public String getContentType() {
			log(DEBUGING, "MediaStreamSource.getContentType()=" + contentType);
			return contentType;
		}
		/**
		 * Get the collection of streams that this source manages. The collection
		 * of streams is entirely content dependent. 
		 */
		public SourceStream[] getStreams() {
			log(DEBUGING, "MediaStreamSource.getStreams()");
			return new SourceStream[] { mediaStream };
		}
		/**
		 * Initiate data-transfer
		 * 
		 * @throws IOException 
		 * 
		 */
		public void start() {
			log(DEBUGING, "MediaStreamSource.start()");
			mediaStreamEnd=false;
			streamLoader = new StreamLoader();
			streamLoader.start();
		}
		/**
		 * Initiate data-transfer from a offset
		 * 
		 * @throws IOException 
		 * 
		 */
		public void startFrom(long where) {
			log(DEBUGING, "MediaStreamSource.startFrom("+where+")");
			mediaStreamEnd=false;
			streamLoader = new StreamLoader();
			streamLoader.skip(where);
			streamLoader.start();
		}
		/**
		 * Stop the data-transfer. If the DataSource has not been connected and 
		 * started, stop is ignored.
		 */
		public void stop() {
			log(DEBUGING, "MediaStreamSource.stop()");
			if (streamLoader != null){
				streamLoader.stop();
			}
			streamLoader = null;
		}
		public Control getControl(String s)	{return null;}
		public Control[] getControls() {return null;}
		/**
		 * Reset input and output stream when seek out of buffer
		 * 
		 */
		public void resetInStream(){
			log(DEBUGING, "MediaStreamSource.resetInStream()");
			if (streamLoader != null && streamLoader.isAlive()) {
				stop();
			}
			if (inStream != null) {
				try {
					inStream.close();
					inStream = null;
					try{
						if (sourceType==CONNECTION_LOCAL){
							inStream = fileConn.openInputStream();
						}
						else if(sourceType==CONNECTION_REMOTE){
							inStream = contentConn.openInputStream();
						}
					}
					catch (IOException e) {
						log(EXCEPTION, "RESET:" + e.toString());
					}
				}
				catch (IOException e) {
					log(EXCEPTION, "RESET:" + e.toString());
				}
				log(DEBUGING, "RESET: reset inStream");
			}
			if (outStream != null && sourceType==CONNECTION_REMOTE) {
				try {
					outStream.close();
					outStream = null;
					outStream = fileConn.openOutputStream();
				}
				catch (IOException e) {
					log(EXCEPTION, "RESET:" + e.toString());
				}
				log(DEBUGING, "RESET: reset outStream");
			}
		}
	}
	/************************* MediaStreamSource End *************************/

	/************************* StreamLoader Begin *************************/
	private class StreamLoader extends Thread {
		/** Total bytes read from data source stream **/
		private int totalLoad;
		/** A flag to stop StreamLoader **/
		private boolean stopStreamLoader = false;
		/** Skip bytes before write to buffer **/
		private long skipBytes;
		
		public StreamLoader(){
			skipBytes = 0;
		}
		public void stop() {
			log(DEBUGING, "StreamLoader.stop()");
			stopStreamLoader = true;
		}
		public void skip(long skipBytes){
			log(DEBUGING, "StreamLoader.skip("+skipBytes+")");
			this.skipBytes = skipBytes;
		}
		public void run() {
			log(DEBUGING, "StreamLoader.run()");
			byte[] data = new byte[READ_CHUNK];
			int readBytes = 0;
			int writeChunk =0;
			int writeOff = 0;
			int writeBytes = 0;
			while (!stopStreamLoader) {
				try {// Read from input stream
					readBytes = inStream.read(data, 0, READ_CHUNK);
				}
				catch (IOException e) {
					log(EXCEPTION, "RUN: inStream.read():" + e.toString());
					break;
				}
				log(INFORMATIONAL, "RUN: inStreamForLoad.read(data," + "0" + "," +
									READ_CHUNK + ")=" + readBytes);
				if (readBytes>0){
					// First write to file
					if (outStream!=null && sourceType==CONNECTION_REMOTE) {
						try {
							outStream.write(data, 0, readBytes);
						}
						catch (IOException e) {
							log(EXCEPTION, "RUN: outStream.write():" + e.toString());
							break;
						}
					}
					// Then skip before write to buffer if player seek
					if (skipBytes>=readBytes){//skip before write into buffer if seek occurs 
						skipBytes -= readBytes;
						totalLoad += readBytes;
						log(INFORMATIONAL, "RUN: Skip " + readBytes);
						continue;
					}
					// Write into buffer
					synchronized(lockSRW){
						writeOff = (int)skipBytes;
						writeChunk = readBytes - (int)skipBytes;
						totalLoad += skipBytes;
						writeBytes = 0;
						skipBytes = 0;
						while(writeChunk>0 && !stopStreamLoader){
							writeBytes = buffer.write(data, writeOff, writeChunk);
							log(INFORMATIONAL, "RUN: buffer.write(data," + writeOff + "," +
												writeChunk + ")=" + writeBytes);
							if (writeBytes==0){
								try {//If buffer full, then wait
									log(INFORMATIONAL, "RUN: Buffer FULL & WAIT");
									notifyPlayerEvent(MediaStreamPlayerListener.BUFFER_FULL, null);
									lockSRW.wait();
								}
								catch (InterruptedException e) {
									log(EXCEPTION, "RUN:" + e.toString());
									break;
								}
							}
							else{
								totalLoad += writeBytes;
								writeOff += writeBytes;
								writeChunk -= writeBytes;
							}
						}
						lockSRW.notifyAll();
					}
				}
				else if (readBytes<0) {
					log(DEBUGING, "RUN: STREAM END RECEIVED!");
					mediaStreamEnd = true;
					break;
				}
				else if (readBytes==0){
					log(ERROR, "RUN: READ ZERO");
				}
			}
			if (totalLoad == contentLength && sourceType==CONNECTION_REMOTE) {
				log(DEBUGING, "RUN: download Complete");
				notifyPlayerEvent(MediaStreamPlayerListener.DOWNLOAD_OK, null);
			}
			else if (totalLoad != contentLength){
				log(ERROR, "RUN: totalRead!=contentLength: "+totalLoad+"!="+contentLength);
			}
			log(DEBUGING, "StreamLoader EXIT");
		}
	}
	/************************* StreamLoader End *************************/

	/************************* MediaBuffer Begin *************************/
	private class MediaBuffer{
		/** A circular buffer queue implemented by the byte array**/
		private volatile byte[] buffer;
		/** Queue tail **/
		private volatile int tail;
		/** Queue head **/
		private volatile int head;
		/** Bytes in the queue **/
		private volatile int bytes;
		/** Reserved bytes for seek **/
		private volatile int resv;
		/** A flag of queue full **/
		private volatile boolean full;
		/** A flag of queue empty **/
		private volatile boolean empty;
		/** Reserve position in the media stream **/
		private volatile long resvPosition;
		/** Tail position in the media stream **/ 
		private volatile long tailPosition;
		/** Head position in the media stream **/
		private volatile long headPosition;
		/**
		 * MediaBuffer Constructor
		 * 
		 * @param bufferSize - the fixed buffer size
		 */
		public MediaBuffer(int bufferSize) {
			buffer = new byte[bufferSize];
			tail = 0;
			head = 0;
			resv = 0;
			bytes = 0;
			resvPosition = 0; 
			tailPosition = 0;
			headPosition = 0;
			full = false;
			empty = true;
		}
		/**
		 * Clear buffer and set buffer to the particular postion
		 * @param pos - the position reset to.
		 */
		public void reset(long pos)	{
			synchronized(this) {
				tail = 0;
				head = 0;
				resv = 0;
				bytes = 0;
				resvPosition = pos;
				tailPosition = pos;
				headPosition = pos;
				full = false;
				empty = true;
			}
		}
		/**
		 * Get the three postions
		 * 
		 * @return An array containing the three positions
		 */
		public long[] getPositions(boolean show) {
			synchronized(this) {
				if (show){
					show();
				}
				return new long[] {resvPosition, tailPosition, headPosition};
			}
		}
		/**
		 * Show buffer information for debug
		 */
		private void show(){
			System.out.println("--------------------------------MediaBuffer--------------------------------");
			System.out.println("MediaBuffer: " + 
							   "tail=" + String.valueOf(tail) +
							   ",head=" + String.valueOf(head) +
							   "[resv:" + String.valueOf(resv) +
							   ",bytes:"+ String.valueOf(bytes)+
							   ",full:" + String.valueOf(full) +
							   ",empt:" + String.valueOf(empty)+ "]");

			System.out.println("MediaBuffer: " + 
							 	"RP=" + String.valueOf(resvPosition) +
							 	",TP=" + String.valueOf(tailPosition) +
							 	",HP=" + String.valueOf(headPosition));
			System.out.println("---------------------------------------------------------------------------");
		}
		/**
		 * Skip the tail position between the reserve position and the head position
		 * 
		 * @param skip
		 */
		public void skip(int skip) {
			synchronized(this) {
				tail = (tail + skip + buffer.length) % buffer.length;
				tailPosition += skip;
				resv += skip;
				bytes -= skip;
				// Full test
				if (resv<=minBufferResv && bytes+resv==buffer.length){
					full = true;
				}
				else{
					full = false;
				}
				// Empty test
				if (bytes==0){
					empty=true;
					show();
				}
				else{
					empty = false;
				}
			}
		}
		/**
		 * Write "len" bytes into buffer.
		 * 
		 * @param data - raw data
		 * @param len - the byte count need to write
		 * @return actura bytes written into buffer
		 */
		public int write(byte[] data, int off, int len) {
			synchronized(this) {
				//observer.updateBufferStatus("Buffer Normal");
				int count = 0;
				for (int i=0; i<len && !full; i++) {
					count++;
					buffer[head] = data[i+off];
					empty = false;
					head = (head+1) % buffer.length;
					headPosition++;
					bytes++;
					if (resv<=minBufferResv){
						if (bytes+resv == buffer.length){
							full = true;
							show();
							return count;
						}
					}
					else if (bytes+resv>buffer.length){
						resv--;
						resvPosition++;
						if(resv==minBufferResv){
							full = true;
							show();
							return count;
						}							
					}
				}
				return count;
			}
		}
		/**
		 * Read "len" bytes from buffer.
		 * 
		 * @param data - the raw data
		 * @param off - the offset of data array
		 * @param len - the byte counts need to read 
		 * @return the actual byte counts read
		 */
		public int read(byte[] data, int off, int len) {
			synchronized(this) {
				int count = 0;
				for (int i=0; i<len && !empty; i++){
					count++;
					data[i+off] = buffer[tail];
					tail = (tail+1) % buffer.length;
					tailPosition++;
					bytes--;
					resv++;
					if (resv > minBufferResv){
						full = false;
					}
					if (bytes==0){
						empty = true;
						show();
						return count;
					}
				}
				return count;
			}	
		}
		/**
		 * Get space left, consider the reserved space
		 * 
		 * @return return space left
		 */
//		public int availableForWrite(){
//			synchronized(this){
//				log(INFORMATIONAL, "MediaBuffer.AFW(): " + 
//									"tail=" + String.valueOf(tail) +
//									",head=" + String.valueOf(head) +
//									"[resv:" + String.valueOf(resv) +
//									",bytes:"+ String.valueOf(bytes)+
//									",full:" + String.valueOf(full) +
//									",empt:" + String.valueOf(empty)+ "]");
//				log(INFORMATIONAL, "MediaBuffer.AFW(): " + 
//									"RP=" + String.valueOf(resvPosition) +
//									",TP=" + String.valueOf(tailPosition) +
//									",HP=" + String.valueOf(headPosition));
//				if (resv > minBufferResv){
//					return buffer.length-bytes-minBufferResv;
//				}
//				else {
//					return buffer.length-bytes-resv;
//				}
//			}
//		}
		/**
		 * Get availale size of buffer
		 * 
		 * @return return the byte counts contained in buffer
		 */
//		public int availableForRead() { 
//			synchronized(this) {
//				log(INFORMATIONAL, "MediaBuffer.AFR(): " + 
//						           "tail=" + String.valueOf(tail) +
//						           ",head=" + String.valueOf(head) +
//						           "[resv:" + String.valueOf(resv) +
//						           ",bytes:"+ String.valueOf(bytes)+
//						           ",full:" + String.valueOf(full) +
//						           ",empt:" + String.valueOf(empty)+ "]");
//				log(INFORMATIONAL, "MediaBuffer.AFR(): " + 
//				                   "RP=" + String.valueOf(resvPosition) +
//						           ",TP=" + String.valueOf(tailPosition) +
//						           ",HP=" + String.valueOf(headPosition));
//				return bytes;
//			}
//		}
	}

	/************************* MediaBuffer End *************************/
	
	/************************* MediaStream Begin *************************/

	private class MediaStream implements SourceStream {
		private ContentDescriptor contentDescriptor;

		public MediaStream(String contentType) {
			log(DEBUGING, "MediaStream()");
			contentDescriptor = new ContentDescriptor(contentType);
		}
		public ContentDescriptor getContentDescriptor()	{
			log(DEBUGING, "MediaStream.getContentDescriptor()");
			return contentDescriptor;
		}
		public long getContentLength() {
			log(DEBUGING, "MediaStream.getContentLength()");
			return contentLength;
		}
		/**
		 * Find out if the stream is seekable. The return value can be one of 
		 * these three: NOT_SEEKABLE, SEEKABLE_TO_START and RANDOM_ACCESSIBLE. 
		 * If the return value is SEEKABLE_TO_START, it means that the stream 
		 * can only be repositioned to the beginning of the stream. If the 
		 * return value is RANDOM_ACCESSIBLE, the stream can be seeked anywhere 
		 * within the stream.
		 */
		public int getSeekType() {
			log(DEBUGING, "MediaStream.getSeekType()=" + RANDOM_ACCESSIBLE);
			//return SEEKABLE_TO_START;
			return RANDOM_ACCESSIBLE;
		}
		/**
		 * 
		 */
		public int read(byte[] data, int off, int len) throws IOException {
			log(INFORMATIONAL, "MediaStream.read(bytes, " + off + ", " +len + ")");
			int readBytes = 0;
			int readChunk = len;
			if (readChunk > readLimit){
				log(INFORMATIONAL, "READ: readLength>readLimit:"+readChunk+">"+readLimit);
				readChunk = readLimit;
			}
			while(true){
				synchronized(lockSRW){
					readBytes = buffer.read(data, off, readChunk);
					log(INFORMATIONAL, "READ: buffer.read(bytes," + off + "," + 
							readChunk + ")=" + readBytes);
					if (readBytes>0){
						if (userSeek){
							notifyPlayerEvent(MediaStreamPlayerListener.USER_SEEK_OK, null);
							userSeek=false;
						}
						lockSRW.notifyAll();
						return readBytes;
					}
					else if (readBytes == 0 && mediaStreamEnd){
						log(DEBUGING, "READ: STREAM END RECEIVED!");
						lockSRW.notifyAll();
						return -1;
					}
					else{
						try {
							log(INFORMATIONAL, "READ: Buffer EMPTY & WAIT");
							notifyPlayerEvent(MediaStreamPlayerListener.BUFFER_EMPTY, null);
							lockSRW.wait();
						}
						catch (InterruptedException e) {
							log(EXCEPTION, "READ: "+e.toString());
						}
					}
				}
			}
		}
		/**
		 * Seek to the specified point in the stream. The seek method may, for a 
		 * variety of reasons, fail to seek to the specified position. For example, 
		 * it may be asked to seek to a position beyond the size of the stream; or 
		 * the stream may only be seekable to the beginning (getSeekType returns 
		 * SEEKABLE_TO_START). The return value indicates whether seeking is 
		 * successful. If it is successful, the value returned will be the same as 
		 * the given position. Otherwise, the return value will indicate what the 
		 * new position is.
		 * 
		 * If the given position is negative, seek will treat that as 0 and attempt 
		 * to seek to 0. An IOException will be thrown if an I/O error occurs, e.g. 
		 * when the stream comes from a remote connection and the connection is broken.
		 * 
		 * @param where - The position to seek to.
		 * 
		 */
		public long seek(long where) throws IOException {
			log(DEBUGING, "MediaStream.seek(" + String.valueOf(where) + ")");
			synchronized(lockSeek){
				synchronized(lockSRW){
					long[] positions = buffer.getPositions(true);
					if (where<positions[0]){
						log(DEBUGING, "SEEK: BEYOND RESV & RESET STREAM");
						mediaStreamSource.resetInStream();
						buffer.reset(where);
						mediaStreamSource.startFrom(where);
					}
					else if (where>=positions[0] && where<=positions[2] && where!=positions[1]){
						log(DEBUGING, "SEEK: IN BUFFER & GREAT");
						buffer.skip((int)(where-positions[1]));
					}
					else if (where>positions[2]){
						log(DEBUGING, "SEEK: BEYOND HEAD & SKIP STREAM");
						buffer.reset(where);
						streamLoader.skip(where-positions[2]);
					}
					lockSRW.notifyAll();
					return where;
				}
			}
		}
		/**
		 * Obtain the current position in the stream.
		 */
		public long tell() {
			long now = buffer.getPositions(true)[1];
			log(DEBUGING, "MediaStream.tell()=" + String.valueOf(now));
			return now;
		}
		public Control getControl(String s) {return null;}
		public Control[] getControls() {return null;}
		/**
		 * Get the size of a "logical" chunk of media data from the source. 
		 * This method can be used to determine the minimum size of the buffer 
		 * to use in conjunction with the read method to read data from the source.
		 */
		public int getTransferSize() { 
			return readLimit;
		}
	}
	/************************* MediaStream End *************************/

	
	
	/************************* PlayerTimer Begin *************************/
	private class PlayerTimer extends Thread {
		private boolean stopTimer;
		public void run() {
			log(DEBUGING, "PlayerTimer.run()");
			long now = 0;
			stopTimer=false;
			while(!stopTimer) {
				now = player.getMediaTime();
				notifyNowPlaying(now);
				try{ 
					Thread.sleep(100);
				} 
				catch(InterruptedException e){
					log(EXCEPTION, e.toString());
				}
			}
			log(DEBUGING, "PlayerTimer EXIT");
		}
		public void stop(){
			log(DEBUGING, "PlayerTimer.stop()");
			stopTimer=true;
		}
	}
	/************************* PlayerTimer End *************************/
	
	/************************* BufferObserver Begin *************************/
	private class BufferObserver extends Thread {
		private boolean stopBufferObserver;
		public void run() {
			log(DEBUGING, "BufferObserver.run()");
			long[] positions = null;
			while(!stopBufferObserver) {
				positions = buffer.getPositions(false);
				notifyNowBuffering(positions);
				try{ 
					Thread.sleep(500);
				} 
				catch(InterruptedException e){
					log(EXCEPTION, e.toString());
				}
			}
			log(DEBUGING, "BufferObserver EXIT");
		}
		public void stop(){
			log(DEBUGING, "BufferObserver.stop()");
			stopBufferObserver = true;
		}
	}
	/************************* PlayerTimer End *************************/
	
	public void playerUpdate(Player player, String event, Object eventData) {
		//log(INFORMATIONAL, "MediaStreamPlayer.playerUpdate(player,"+event+","+eventData+")");
		if(listener!=null) {
			listener.playerUpdate(event, eventData);
		}
		if(event.equalsIgnoreCase(PlayerListener.ERROR)){
			log(ERROR, "EVENT: close due to error: " + eventData);
			try {
				close();
			}
			catch (MediaException e) {
				log(EXCEPTION, e.toString());
			}
		}
	}
	
	private void notifyNowPlaying(long now){
		if (listener != null) {
			listener.updatePlayingPosition(now);
		}
	}
	private void notifyNowBuffering(long[] positions) {
		if (listener != null){
			listener.updateBufferPosition(positions);
		}
	}
	private void notifyPlayerEvent(String event, Object eventData) {
		if (listener != null){
			listener.playerUpdate(event, eventData);
		}		
	}
}
